﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using TracedReality.RayTracing;

namespace TracedReality.Primitive
{
    /// <summary>
    /// An axis-aligned bounding box.
    /// </summary>
    public class BoundingBox
    {
        /// <summary>
        /// The minimum corner of this BoundingBox.
        /// </summary>
        public Vector min;

        /// <summary>
        /// The maximum corner of this BoundingBox.
        /// </summary>
        public Vector max;

        /// <summary>
        /// The center of the volume contained in this BoundingBox.
        /// </summary>
        public Vector center { get { return min + (0.5 * (max - min)); } }

        public Vector size { get { return max - min; } }

        /// <summary>
        /// Create a BoundingBox with the given points.
        /// The resulting BoundingBox is the minimum and maximum of the two points.
        /// </summary>
        /// <param name="p1">The first point.</param>
        /// <param name="p2">The second point.</param>
        public BoundingBox(Vector p1, Vector p2)
        {
            min = Vector.Min(p1, p2);
            max = Vector.Max(p1, p2);
        }

        public BoundingBox(BoundingBox o)
        {
            min = new Vector(o.min);
            max = new Vector(o.max);
        }

        public double volume()
        {
            Vector diff = max - min;
            return diff.x * diff.y * diff.z;
        }

        public void translate(Vector t)
        {
            min += t;
            max += t;
        }

        /// <summary>
        /// Check if this BoundingBox contains a finite volume.
        /// </summary>
        /// <returns>True if the volume contained is finite, false otherwise.</returns>
        public bool isFinite()
        {
            Vector d = max - min;

            if (double.IsInfinity(d.x) || double.IsNaN(d.x)) { return false; }
            if (double.IsInfinity(d.y) || double.IsNaN(d.y)) { return false; }
            if (double.IsInfinity(d.z) || double.IsNaN(d.z)) { return false; }

            return true;
        }

        /// <summary>
        /// Extend this BoundingBox to contain the given point.
        /// </summary>
        /// <param name="v">The point to extend to.</param>
        public void extend(Vector v)
        {
            min = Vector.Min(min, v);
            max = Vector.Max(max, v);
        }

        /// <summary>
        /// Extend this BoundingBox to contain the given BoundingBox.
        /// </summary>
        /// <param name="b">The BoundingBox to extend to.</param>
        public void extend(BoundingBox b)
        {
            extend(b.min);
            extend(b.max);
        }

        /// <summary>
        /// Check if this BoundingBox overlaps another.
        /// </summary>
        /// <param name="b">The other BoundingBox to check.</param>
        /// <returns>True if the BoundingBoxes overlap, false otherwise.</returns>
        public bool overlaps(BoundingBox b)
        {
            if (min.x > b.max.x) { return false; }
            if (b.min.x > max.x) { return false; }
            if (min.y > b.max.y) { return false; }
            if (b.min.y > max.y) { return false; }
            if (min.z > b.max.z) { return false; }
            if (b.min.z > max.z) { return false; }

            return true;
        }

        /// <summary>
        /// Check if another BoundingBox is entirely contained within this BoundingBox.
        /// </summary>
        /// <param name="b">The other BoundingBox to check.</param>
        /// <returns>True if this BoundingBox contains the other, false otherwise.</returns>
        public bool contains(BoundingBox b)
        {
            return contains(b.min) && contains(b.max);
        }

        /// <summary>
        /// Check if this BoundingBox contains the given point.
        /// </summary>
        /// <param name="p">The point to check.</param>
        /// <returns>True if this BoundingBox contains the point, false otherwise.</returns>
        public bool contains(Vector p)
        {
            if (p.x < max.x && p.x >= min.x &&
                p.y < max.y && p.y >= min.y &&
                p.z < max.z && p.z >= min.z) { return true; }

            return false;
        }

        /// <summary>
        /// Used for intersection. Finds the near and far t-values for the given Ray along
        /// the given dimension.
        /// </summary>
        /// <param name="interval">The initial interval along the Ray.</param>
        /// <param name="r">The Ray to check.</param>
        /// <param name="dim">Which dimension to check along.</param>
        /// <returns>The new interval along the Ray where intersection occurs.</returns>
        private double[] getInSlab(double[] interval, Ray r, int dim)
        {
            double d1 = 0;
            double d2 = 0;
            double n1 = 0;
            double n2 = 0;
            switch (dim)
            {
                case 0:
                    d1 = r.direction.dot(new Vector(1, 0, 0));
                    n1 = (min - r.origin).dot(new Vector(1, 0, 0));

                    d2 = r.direction.dot(new Vector(-1, 0, 0));
                    n2 = (max - r.origin).dot(new Vector(-1, 0, 0));
                    break;
                case 1:
                    d1 = r.direction.dot(new Vector(0, 1, 0));
                    n1 = (min - r.origin).dot(new Vector(0, 1, 0));

                    d2 = r.direction.dot(new Vector(0, -1, 0));
                    n2 = (max - r.origin).dot(new Vector(0, -1, 0));
                    break;
                case 2:
                    d1 = r.direction.dot(new Vector(0, 0, 1));
                    n1 = (min - r.origin).dot(new Vector(0, 0, 1));

                    d2 = r.direction.dot(new Vector(0, 0, -1));
                    n2 = (max - r.origin).dot(new Vector(0, 0, -1));
                    break;
            }

            double t1 = -1;
            double t2 = Double.PositiveInfinity;

            if (d1 != 0)
            {
                t1 = n1 / d1;
            }
            if (d2 != 0)
            {
                t2 = n2 / d2;
            }

            if (t1 > t2) { double t = t2; t2 = t1; t1 = t; }

            double[] result = new double[2];
            result[0] = Math.Max(interval[0], t1);
            result[1] = Math.Min(interval[1], t2);

            return result;
        }

        /// <summary>
        /// Check if the given Ray intersects this BoundingBox.
        /// </summary>
        /// <param name="r">Ray to check for intersection.</param>
        /// <param name="h">Hit to check for earlier intersection</param>
        /// <returns>True if the Ray intersects this BoundingBox, false otherwise.</returns>
        public bool intersects(Ray r, Hit h = null)
        {
            double[] interval = new double[] { 0, Double.PositiveInfinity };
            for (int a = 0; a < 3; a++) { interval = getInSlab(interval, r, a); }

            if (interval[0] > interval[1]) { return false; }
            if (h == null) { return true; }
            return interval[0] < h.t;
        }

        /// <summary>
        /// This is a horrible ToString that I don't think is ever used.
        /// </summary>
        /// <returns></returns>
        public override string ToString()
        {
            return min + "\t" + max;
        }

        public static BoundingBox Minimum(List<BoundingBox> boxes)
        {
            if (boxes.Count == 0) { return new BoundingBox(new Vector(0), new Vector(Settings.EPSILON)); }

            BoundingBox result = new BoundingBox(boxes[0]);
            foreach (BoundingBox b in boxes) { result.extend(b); }

            return result;
        }
    }
}
